Effective C++ 条款27 类型转换

条款27 : 尽量少做转型动作

优良的C++代码很少使用转型, 但是要完全摆脱它们又太过不切实际, 我们应当保证”类型错误”绝无可能发生. 本条款在了解各种转型动作的前提下, 指出了一些有风险的转型操作及其解决方法.

我们先来回顾一下类型转换 :

旧式C风格转型

1
2
(T)expression;
T(experssion);

新式转型

1
2
3
4
static_cast<T>(expression)
const_cast<T>(expression)
dynamic_cast<T>(expression)
reinterpret_cast<T>(expression)
  • static_cast : 强制隐式转换, 适合除常量性移除之外的大部分转换场景.
  • const_cast : 唯一可以常量性移除的转换, 而且能且仅能移除常量性.
  • dynamic_cast : “安全向下转型”, 可以理解为一个有安全类型检查的可以将基类转为派生类的static_cast.
  • reinterpret_cast : 低级转型, 现在很少用了.

书中提出第一个观点 : 除非你确定转型没有任何风险, 并且旧式转型很方便, 应当始终理智使用新式转型, 以下为原因 :

  1. 新式转型很容易在代码中被识别出来, 有助于编译器或其他工具识别.
  2. 新式转型细化了转型动作的目标, 使得编译器更容易诊断出错误的运用.

避免做出”对象在C++中如何布局”的假设

书中提醒我们, 类型转换并不仅仅是“告诉编译器类型的变化”, 它是会实际产生额外代码的, 而且有可能对当前对象布局做出调整, 例如将基类指针从原本的指向的基类, 改为指向派生类, 我们也许会认为前者和后者的指针地址是一样的, 实际也确实大多数情况都是一样的(包括我测试的), 但是书中说对象布局方式会依照编译器的不同而不同, 确实会发生前后者指针不一样的情况出现, 而且在出现多重继承是这种情况会更多. 所以作者告诫我们, “由于知道对象如何布局”而设计的转型, 在某些平台行得通, 在其他平台并不一定.


避免用类型转换写出似是而非的代码

在写GUI时会有定义很多的窗口类, 这种应用框架一般会坚持派生类的重写会先调用父类版本, 于是就有了以下代码 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Window {                                // base class
public:
virtual void onResize() { ... } // 基类窗口重置尺寸
...
};

class SpecialWindow: public Window { // derived class
public:
virtual void onResize() { // 派生类重写
static_cast<Window>(*this).onResize(); // 先调用父类版本的onResize()
...
}
...
};

我们起初看可能还像一回事, 但是实际漏洞百出, static_cast<Window>(*this)这个表达式返回的其实是一个**”this对象值base class成分”的临时拷贝*! static_cast在底层转换类型后进行切分, 然后把切分出来的部分拷贝返回. 也就是说真正调用基类onResize()的是这个临时对象, 而不是当前对象的基类部分, 想要真正使当前对象调用基类onResize(), 应以如下写法 :

1
2
3
4
5
6
7
8
class SpecialWindow: public Window {
public:
virtual void onResize() {
Window::onResize(); // 这样调用
...
}
...
};

谨慎使用dynamic_cast

我们之所以需要dynamic_cast, 通常是因为我们手持一个静态类型是base而动态类型是derived的指针或引用时, 想要使用只有derived中有的一个普通函数, 因为其不是虚函数, 所以我们现在无权使用它, 只能依靠dynamic_cast来进行较为安全的转换.

假设我们的SpecialWindow有一个单独的闪烁功能, 有一个vector中存放了大量的Window, 我们希望遍历该vector, 如果其动态类型是SpecialWindow就调用其闪烁功能, 代码如下 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Window { ... };
class SpecialWindow: public Window {
public:
void blink(); // 特有的blink功能
...
};

typedef std::vector<std::shared_ptr<Window> > VPW; // 存放Window智能指针的数组
VPW winPtrs;
...
for (VPW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter) {
// dynamic_cast转换成功说明是SpecialWindow类型, psw不为空, 判true, 调用blink
// 转换失败说明不是, psw为nullptr, 判false
if (SpecialWindow *psw = dynamic_cast<SpecialWindow*>(iter->get()))
psw->blink();
}

上面算是对dynamic_cast的日常用法做了一个介绍, 但是作者告诉你使用dynamic_cast在很多情况并不必要, 能不使用dynamic_cast就尽量不使用, 原因在于dynamic_cast为了实现安全性检测和其他一些目的, 效率十分低下, 相比于它的其他几个兄弟效率不只低了一点半点, 除非有必须是多态情况下用基类使用派生类普通函数的场景, 我们可以做出一些让步来提升我们的效率, 书中给出了两种方法 :

  1. 放弃多态需求, 确保容器内只有需求派生类的指针.具体来说就是VPW内只存shared_ptr<SpecialWindow>, 让blink的调用普遍化.

    1
    2
    3
    4
    5
    typedef std::vector<std::tr1::shared_ptr<SpecialWindow> > VPSW; // 只存派生类智能指针
    VPSW winPtrs;
    ...
    for (VPSW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter)
    (*iter)->blink(); // 直接调用
  2. 将普通函数转为virtual函数, 略微增加写代码的成本.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class Window {
    public:
    virtual void blink() {} // 这里什么都不做, 其目的只是为了让SpecialWindow通过多态调用有效果的blink()
    ...
    };

    class SpecialWindow: public Window {
    public:
    virtual void blink() { ... };
    ...
    };

    typedef std::vector<std::tr1::shared_ptr<Window> > VPW;
    VPW winPtrs;
    ...
    for (VPW::iterator iter = winPtrs.begin();
    iter != winPtrs.end();
    ++iter)
    (*iter)->blink(); // 正常的多态用法

最后总结下来就是, 在出现dynamic_cast需求场景时, 如果代码对效率没有太多需求, 直接使用dynamic_cast, 反之则思考是否能做出上面所说的两种让步, 如果前两种让步的代价实在太高再使用dynamic_cast.

书中还指出了一种应当杜绝的写法 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Window { ... };
class SpecialWindow1 { ... };
class SpecialWindow2 { ... };
class SpecialWindow3 { ... };
...
typedef std::vector<std::tr1::shared_ptr<Window> > VPW;
VPW winPtrs;
...

for (VPW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter)
{
if (SpecialWindow1 *psw1 =
dynamic_cast<SpecialWindow1*>(iter->get())) { ... }
else if (SpecialWindow2 *psw2 =
dynamic_cast<SpecialWindow2*>(iter->get())) { ... }
else if (SpecialWindow3 *psw3 =
dynamic_cast<SpecialWindow3*>(iter->get())) { ... }
...
}

这样子的代码, 不仅效率低, 而且可维护性差, 最好将这种需求改写成某种virtual函数来实现, 不要去依赖dynamic_cast.


请记住 :

  • 尽量避免转型, 尤其在注重效率的程序中避免dynamic_cast.
  • 转型并非什么都没做, 会产生实际的花销.
  • 如果转型是必要的, 可以把转型过程放在一个函数中, 让客户调用该函数以实现转型, 而不需要将转型写入他们的代码.
  • 最好使用C++风格的新式转型.

by 天目中云


Effective C++ 条款27 类型转换
http://example.com/2024/12/06/[Effective C++]条款27 类型转换/
作者
天目中云
发布于
2024年12月6日
许可协议